/*
 * CC3Camera.m
 *
 * $Version: cocos3d 0.5.2 (f3df37821a3f) on 2011-03-13 $
 * Author: Bill Hollings
 * Copyright (c) 2010-2011 The Brenwill Workshop Ltd. All rights reserved.
 * http://www.brenwill.com
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * http://en.wikipedia.org/wiki/MIT_License
 * 
 * See header file CC3Camera.h for full API documentation.
 */

#import "CC3Camera.h"
#import "CC3World.h"
#import "CC3Math.h"
#import "CC3OpenGLES11Engine.h"
#import "CGPointExtension.h"


#pragma mark CC3Camera implementation

@interface CC3Node (TemplateMethods)
-(void) buildTransformMatrix;
-(void) populateFrom: (CC3Node*) another;
@end

@interface CC3Camera (TemplateMethods)
@property(nonatomic, readonly) CC3ViewportManager* viewportManager;
-(void) buildModelViewMatrix;
-(void) buildProjectionMatrix;
-(void) buildFrustum;
-(void) openProjection;
-(void) closeProjection;
-(void) openModelView;
-(void) closeModelView;
-(void) loadProjectionMatrix;
-(void) loadModelViewMatrix;
@property(nonatomic, readonly) BOOL isProjectionDirty;
@property(nonatomic, readonly) BOOL isFrustumDirty;
@end


@implementation CC3Camera

@synthesize fieldOfView, nearClippingPlane, farClippingPlane;
@synthesize world, frustum, modelviewMatrix, projectionMatrix;

-(void) dealloc {
	world = nil;						// not retained
	[modelviewMatrix release];
	[projectionMatrix release];
	[frustum release];
	[super dealloc];
}

-(void) setFieldOfView:(GLfloat) anAngle {
	fieldOfView = anAngle;
	isProjectionDirty = YES;
}

-(void) setNearClippingPlane:(GLfloat) aDistance {
	nearClippingPlane = aDistance;
	isProjectionDirty = YES;
}

-(void) setFarClippingPlane:(GLfloat) aDistance {
	farClippingPlane = aDistance;
	isProjectionDirty = YES;
}

// The CC3World's viewport manager.
-(CC3ViewportManager*) viewportManager {
	return world.viewportManager;
}

-(BOOL) isTransformRigid {
	return (parent ? parent.isTransformRigid : YES);
}


#pragma mark Allocation and initialization

-(id) initWithTag: (GLuint) aTag withName: (NSString*) aName {
	if ( (self = [super initWithTag: aTag withName: aName]) ) {
		self.modelviewMatrix = [CC3GLMatrix identity];
		self.projectionMatrix = [CC3GLMatrix identity];
		self.frustum = [CC3Frustum frustum];
		isProjectionDirty = NO;		// start with identity everywhere
		isFrustumDirty = YES;		// need to calculate this first time
		fieldOfView = kCC3DefaultFieldOfView;
		nearClippingPlane = kCC3DefaultNearClippingPlane;
		farClippingPlane = kCC3DefaultFarClippingPlane;
	}
	return self;
}

// Protected properties for copying
-(BOOL) isProjectionDirty { return isProjectionDirty; }
-(BOOL) isFrustumDirty { return isFrustumDirty; }

// Template method that populates this instance from the specified other instance.
// This method is invoked automatically during object copying via the copyWithZone: method.
-(void) populateFrom: (CC3Camera*) another {
	[super populateFrom: another];

	[modelviewMatrix release];
	modelviewMatrix = [another.modelviewMatrix copy];		// retained
	
	[projectionMatrix release];
	projectionMatrix = [another.projectionMatrix copy];		// retained
	
	[frustum release];
	frustum = [another.frustum copy];						// retained

	fieldOfView = another.fieldOfView;
	nearClippingPlane = another.nearClippingPlane;
	farClippingPlane = another.farClippingPlane;
	isProjectionDirty = another.isProjectionDirty;
	isFrustumDirty = another.isFrustumDirty;
}

-(NSString*) fullDescription {
	return [NSString stringWithFormat: @"%@, FOV: %.2f, near: %.2f, far: %.2f",
			[super fullDescription], fieldOfView, nearClippingPlane, farClippingPlane];
}


#pragma mark Transformations

-(void) markProjectionDirty {
	isProjectionDirty = YES;
}

/**
 * Scaling the camera is a null operation because it scales everything, including the size
 * of objects, but also the distance from the camera to those objects. The effects cancel
 * out, and visually, it appears that nothing has changed. Therefore, the scale property
 * is not applied to the transform matrix of the camera. Instead it is used to adjust the
 * field of view to create a zooming effect. See the description of the fieldOfView property.
 *
 * This implementation sets the globalScale to that of the parent node, or to unit scaling
 * if no parent. The globalScale is then used to unwind all scaling from the camera, globally,
 * because any inherited scaling will scale the frustum, and cause undesirable clipping
 * artifacts, particularly at the near clipping plane.
 *
 * For example, if the camera is mounted on another node that is scaled to ten times, the
 * near clipping plane of the camera will be scaled away from the camera by ten times,
 * resulting in unwanted clipping around the fringes of the view. For this reason, an inverse
 * scale of 1/10 is applied to the transform to counteract this effect.
 */
-(void) applyScaling {
	globalScale = parent ? parent.globalScale : kCC3VectorUnitCube;
	[transformMatrix scaleBy: CC3VectorInvert(globalScale)];
	LogTrace(@"%@ scaled back by global %@ to counter parent scaling %@",
			 self, NSStringFromCC3Vector(globalScale), transformMatrix);
}

-(void) buildPerspective {
	[self trackTarget];
	[self buildProjectionMatrix];
	[self buildFrustum];
}

/** Overridden to also build the modelview matrix and the frustum. */
-(void) buildTransformMatrix {
	[super buildTransformMatrix];
	[self buildModelViewMatrix];
	isFrustumDirty = YES;
}

/**
 * Template method to rebuild the modelviewMatrix from the deviceRotationMatrix, which
 * is managed by the CC3World's viewportManager, and the inverse of the transformMatrix.
 * Invoked automatically whenever the transformMatrix or device orientation are changed.
 */
-(void) buildModelViewMatrix {
	[modelviewMatrix populateFrom: self.viewportManager.deviceRotationMatrix];
	LogTrace(@"%@ applied device rotation matrix %@", self, modelviewMatrix);
	
	CC3GLMatrix* tm = [transformMatrix copyAutoreleased];

	// If the transform is rigid (only rotation & translation), use faster inversion.
	// TODO: it would be better to move this test to the matrix itself, but there is no
	//       known quick and accurate test to tell if a matrix represents a rigid transform
	if (self.isTransformRigid) {
		[tm invertRigid];
	} else {
		[tm invertAffine];
	}
	LogTrace(@"%@ transform %@ inverted %@to %@", self, self.transformMatrix,
			 (self.isTransformRigid ? @"rigidly " : @""), tm);

	[modelviewMatrix multiplyByMatrix: tm];
	LogTrace(@"%@ inverted transform applied to modelview matrix %@", self, modelviewMatrix);
}

/**
 * Template method to rebuild the projectionMatrix if the projection
 * parameters have been changed since the last rebuild.
 */
-(void) buildProjectionMatrix  {
	if(isProjectionDirty) {
		CC3Viewport vp = self.viewportManager.viewport;
		LogTrace(@"Setting GL perspective from FOV: %.2f, Width: %i, Height: %i, Near: %.2f, Far: %.2f, Zoom: %.2f",
				 fieldOfView, vp.w, vp.h, nearClippingPlane, farClippingPlane, self.uniformScale);
		NSAssert(vp.h, @"Camera projection matrix cannot be updated before setting the viewport");
		[projectionMatrix populatePerspectiveFromFOV: fieldOfView
										   andAspect: ((GLfloat) vp.w / (GLfloat) vp.h)
										 andNearClip: nearClippingPlane
										  andFarClip: farClippingPlane
											 andZoom: self.uniformScale];
		isProjectionDirty = NO;
		isFrustumDirty = YES;
	}
}

/**
 * Template method to rebuild the frustum if either the transformMatrix
 * or the projectionMatrix have changed since the frustum was last rebuilt.
 */
-(void) buildFrustum {
	if (isFrustumDirty) {
		[frustum populateFromProjection: projectionMatrix andModelView: modelviewMatrix];
		LogTrace(@"%@ building %@ from projection %@ and modelview %@",
				 self, frustum, projectionMatrix, modelviewMatrix);
		isFrustumDirty = NO;
	}
}


#pragma mark Drawing

-(void) open {
	LogTrace(@"Opening %@", self);
	[self openProjection];
	[self openModelView];
}

-(void) close {
	LogTrace(@"Closing %@", self);
	[self closeModelView];
	[self closeProjection];
}

/** Template method that pushes the GL projection matrix stack, and loads the projectionMatrix into it. */
-(void) openProjection {
	LogTrace(@"Opening %@ 3D projection", self);
	[[CC3OpenGLES11Engine engine].matrices.projection push];
	[self loadProjectionMatrix];
}

/** Template method that pops the projectionMatrix from the GL projection matrix stack. */
-(void) closeProjection {
	LogTrace(@"Closing %@ 3D projection", self);
	[[CC3OpenGLES11Engine engine].matrices.projection pop];
}

/** Template method that pushes the GL modelview matrix stack, and loads the modelviewMatrix into it. */
-(void) openModelView {
	LogTrace(@"Opening %@ modelview", self);
	[[CC3OpenGLES11Engine engine].matrices.modelview push];
	[self loadModelViewMatrix];
}

/** Template method that pops the modelviewMatrix from the GL modelview matrix stack. */
-(void) closeModelView {
	LogTrace(@"Closing %@ modelview", self);
	[[CC3OpenGLES11Engine engine].matrices.modelview pop];
}

/** Template method that loads the modelviewMatrix into the current GL projection matrix. */
-(void) loadModelViewMatrix {
	LogTrace(@"%@ loading modelview matrix into GL: %@", self, modelviewMatrix);
	[[CC3OpenGLES11Engine engine].matrices.modelview load: modelviewMatrix.glMatrix];
}

/** Template method that loads the projectionMatrix into the current GL projection matrix. */
-(void) loadProjectionMatrix {
	LogTrace(@"%@ loading projection matrix into GL: %@", self, projectionMatrix);
	[[CC3OpenGLES11Engine engine].matrices.projection load: projectionMatrix.glMatrix];
}


#pragma mark 3D <-> 2D mapping functionality

-(CC3Vector) projectNode: (CC3Node*) aNode {
	NSAssert(aNode, @"Camera cannot project a nil node.");
	CC3Vector pLoc = [self projectLocation: aNode.globalLocation];
	aNode.projectedLocation = pLoc;
	return pLoc;
}

-(CC3Vector) projectLocation: (CC3Vector) a3DLocation {
	
	// Convert specified location to a 4D homogeneous location vector
	// and transform it using the modelview and projection matrices.
	CC3Vector4 hLoc = CC3Vector4FromCC3Vector(a3DLocation, 1.0);
	hLoc = [modelviewMatrix transformHomogeneousVector: hLoc];
	hLoc = [projectionMatrix transformHomogeneousVector: hLoc];
	
	// Convert projected 4D vector back to 3D.
	CC3Vector projectedLoc = CC3VectorFromCC3Vector4(hLoc);

	// The projected vector is in a projection coordinate space between -1 and +1 on all axes.
	// Normalize the vector so that each component is between 0 and 1 by calculating ( v = (v + 1) / 2 ).
	projectedLoc = CC3VectorScaleUniform(CC3VectorAdd(projectedLoc, kCC3VectorUnitCube), 0.5f);
	
	// Map the X & Y components of the projected location (now between 0 and 1) to viewport coordinates.
	CC3Viewport vp = self.viewportManager.viewport;
	projectedLoc.x = vp.x + (vp.w * projectedLoc.x);
	projectedLoc.y = vp.y + (vp.h * projectedLoc.y);
	
	// Using the vector from the camera to the 3D location, determine whether or not the
	// 3D location is in front of the camera by using the dot-product of that vector and
	// the direction the camera is pointing. Set the Z-component of the projected location
	// to be the signed distance from the camera to the 3D location, with a positive sign
	// indicating the location is in front of the camera, and a negative sign indicating
	// the location is behind the camera.
	CC3Vector camToLocVector = CC3VectorDifference(a3DLocation, self.globalLocation);
	GLfloat camToLocDist = CC3VectorLength(camToLocVector);
	GLfloat frontOrBack = SIGN(CC3VectorDot(camToLocVector, self.forwardDirection));
	projectedLoc.z = frontOrBack * camToLocDist;
	
	// Map the projected point to the device orientation then return it
	CGPoint ppt = [self.viewportManager cc2PointFromGLPoint: ccp(projectedLoc.x, projectedLoc.y)];
	CC3Vector orientedLoc = cc3v(ppt.x, ppt.y, projectedLoc.z);
	
	LogTrace(@"%@ projecting location %@ to %@ and orienting with device to %@ using viewport %@",
			 self, NSStringFromCC3Vector(a3DLocation), NSStringFromCC3Vector(projectedLoc),
			 NSStringFromCC3Vector(orientedLoc), NSStringFromCC3Viewport(self.viewportManager.viewport));
	return orientedLoc;
}

@end


#pragma mark -
#pragma mark CC3Frustum implementation

@implementation CC3Frustum

@synthesize top, bottom, left, right, near, far;

+(id) frustum {
	return [[[self alloc] init] autorelease];
}

// Template method that populates this instance from the specified other instance.
// This method is invoked automatically during object copying via the copyWithZone: method.
-(void) populateFrom: (CC3Frustum*) another {
	top = another.top;
	bottom = another.bottom;
	left = another.left;
	right = another.right;
	near = another.near;
	far = another.far;
}

-(id) copyWithZone: (NSZone*) zone {
	CC3Frustum* aCopy = [[[self class] allocWithZone: zone] init];
	[aCopy populateFrom: self];
	return aCopy;
}

-(void) populateFromProjection:  (CC3GLMatrix*) aProjectionMatrix
				  andModelView: (CC3GLMatrix*) aModelViewMatrix {

	CC3GLMatrix* mvp = [aProjectionMatrix copyAutoreleased];
	[mvp multiplyByMatrix: aModelViewMatrix];

	GLfloat* m = mvp.glMatrix;

	bottom = CC3PlaneNormalize(CC3PlaneMake(m[3]+m[1], m[7]+m[5], m[11]+m[9], m[15]+m[13]));
	top    = CC3PlaneNormalize(CC3PlaneMake(m[3]-m[1], m[7]-m[5], m[11]-m[9], m[15]-m[13]));

	left   = CC3PlaneNormalize(CC3PlaneMake(m[3]+m[0], m[7]+m[4], m[11]+m[8], m[15]+m[12]));
	right  = CC3PlaneNormalize(CC3PlaneMake(m[3]-m[0], m[7]-m[4], m[11]-m[8], m[15]-m[12]));

	near   = CC3PlaneNormalize(CC3PlaneMake(m[3]+m[2], m[7]+m[6], m[11]+m[10], m[15]+m[14]));
	far    = CC3PlaneNormalize(CC3PlaneMake(m[3]-m[2], m[7]-m[6], m[11]-m[10], m[15]-m[14]));
}

-(BOOL) doesIntersectPointAt: (CC3Vector) location {
	// Treat the point as a sphere of zero radius.
	return [self doesIntersectSphereAt: location withRadius: 0.0];
}

-(BOOL) doesIntersectSphereAt: (CC3Vector) location withRadius: (GLfloat) radius {
	GLfloat dist;
	
	// The sphere will be outside the frustum if it lies farther behind any one of the
	// planes than its radius.
	// Determine the distance from the location to each plane in the frustum and return NO
	// if the location is farther behind any of the planes by the length of the radius.  
	dist = CC3DistanceFromNormalizedPlane(top, location);
	if (dist + radius < 0) {
		return NO;
	}
	dist = CC3DistanceFromNormalizedPlane(bottom, location);
	if (dist + radius < 0) {
		return NO;
	}
	dist = CC3DistanceFromNormalizedPlane(right, location);
	if (dist + radius < 0) {
		return NO;
	}
	dist = CC3DistanceFromNormalizedPlane(left, location);
	if (dist + radius < 0) {
		return NO;
	}
	dist = CC3DistanceFromNormalizedPlane(near, location);
	if (dist + radius < 0) {
		return NO;
	}
	dist = CC3DistanceFromNormalizedPlane(far, location);
	if (dist + radius < 0) {
		return NO;
	}
	
	return YES;		// Not behind any of the planes, so must be inside the frustum
}

-(NSString*) description {
	return [NSString stringWithFormat: @"%@", [self class]];
}

-(NSString*) fullDescription {
	return [NSString stringWithFormat: @"%@ top: %@ bottom: %@ left: %@ right: %@ near: %@ far: %@",
										[self class],
										NSStringFromCC3Plane(top), NSStringFromCC3Plane(bottom),
										NSStringFromCC3Plane(left), NSStringFromCC3Plane(right),
										NSStringFromCC3Plane(near), NSStringFromCC3Plane(far)];
}

@end
